<!DOCTYPE html>

<!--
  Accessibility Object Model Presentation
  2017

  Based on previous HTML slide presentation code authored by:
  Dominic Mazzoni, Rachel Shearer, Luke Mahé and Marcin Wichary.

  Copyright Google Inc.

  Licensed under the Apache License, Version 2.0 (the "License");
  you may not use this file except in compliance with the License.
  You may obtain a copy of the License at

  http://www.apache.org/licenses/LICENSE-2.0

  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS,
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  See the License for the specific language governing permissions and
  limitations under the License.
-->

<html>

<head>
  <title>Accessibility Object Model</title>
  <meta charset="utf-8" />
  <script src="slides.js"></script>
  <script src="aom_slides.js"></script>
  <link rel="stylesheet" href="aom_styles.css">
</head>

<body style="display: none" onload="initAriaPreso()">
  <section class="slides" onkeydown="return overrideKeys(event)">

    <article class="title">
      <h1>
        Accessibility Object Model
      </h1>
      <p>
        <b>Specification Editors:</b>
        <br> Alexander Surkov, Mozilla
        <br> Alice Boxhall, Google
        <br> Dominic Mazzoni, Google
        <br> James Craig, Apple
      </p>
    </article>

    <article>
      <h3>
        Presentation Check
      </h3>
      <div style="padding-right: 30%;">
        <div class="aom_enabled">
          <p class="unicode_background" style="color: #99ee99;" title="Check mark indicates AOM is enabled in this browser">
            &#x2714;
          </p>
          <p>
            Great! The Accessibility Object Model is enabled in this browser.
          </p>
          <p>
            Demos should work fine.
          </p>
        </div>
        <div class="aom_disabled">
          <p class="unicode_background" style="color: #ee9999;" title="Red X indicates AOM is not enabled and some slides won't work">
            &#x2718;
          </p>
          <p>
            Accessibility Object Model support is not enabled. Interactive features of these slides won't work.
          </p>
          <p>
            To enable it in Chrome, use this command-line switch:
          </p>
          <pre>
        --enable-blink-features=AccessibilityObjectModel
      </pre>
          <p>
            To enable it in Safari Technology Preview, select
            <br>
            <a href="safari-aom.png">
              Develop > Experimental Features > Accessibility Object Model.
            </a>
          </p>
        </div>
      </div>
      <p style="width:60%">
        Tip: Arrow keys move between slides, but not when something else has focus.
      </p>
      <p>
        Page Up / Down moves between slides, regardless of focus.
      </p>
    </article>

    <article>
      <h3>
        Overview
      </h3>
      <ul>
        <li>Background</li>
        <li>Motivating Use Cases</li>
        <li>Phase 1: Properties</li>
        <li>Phase 2: Actions / Input Events</li>
        <li>Phase 3: Virtual Nodes</li>
        <li>Phase 4: Introspection</li>
        <li>Q&amp;A</li>
      </ul>
    </article>

    <article class="medimage">
      <h3>
        Background
      </h3>
      <p>
        Assistive technology (AT) augments or replaces the existing UI for an application. Examples include:
        <ul>
          <li>Screen reader
            <li>Voice Control
              <li>Switch Access
        </ul>
      </p>
      <img src="../images/a11y-tree.png" alt="Image showing 4 layers of 2-way communication - from the application, to the accessibility tree, to assistive technology, to the user.">
    </article>

    <article>
      <h3>
        Accessibility APIs
      </h3>
      <p>
        Each operating system has its own accessibility APIs for native applications.
      </p>
      <ul>
        <li>
          <b>Windows</b>: MSAA, IAccessible2, UI Automation
          <li>
            <b>macOS</b>: NSAccessibility
            <li>
              <b>iOS</b>: UIAccessibility
              <li>etc.
      </ul>
      <p>
        Besides assistive technology, these APIs are also often used for
        <em>automation</em>, for example for password managers or for single-sign-on.
      </p>
    </article>

    <article class="medimage">
      <h3>
        Accessibility Tree
      </h3>
      <p>
        Native applications expose an
        <em>accessibility tree</em> via accessibility APIs. Each
        <em>accessibility node</em> has properties such as a
        <b>role</b>,
        <b>name</b>,
        <b>state</b>, and
        <b>actions</b>.
      </p>
      <img src="../images/a11y-node.png" alt="Visualization of an accessibility tree. The root has role main, the child has role form, then it has three children, a radio button, a slider, and a button, each with role, name, state, and actions pictured.">
    </article>

    <article class="largeimage">
      <h3>
        Accessibility on the web
      </h3>
      <p>
        Web browsers automatically map the DOM into the accessibility tree.
      </p>
      <img src="../images/DOM-a11y-tree.png" alt="Image showing how the web browser simultaneously paints the visual UI and also updates the accessibility tree.">
    </article>

    <article class="largeimage">
      <h3>
        Web accessibility APIs
      </h3>
      <p>
        The web has rich support for making applications accessible, but only via a declarative API.
      </p>
      <p>
        There's always a one-to-one correspondence between a DOM node and a node in the accessibility tree.
      </p>
      <img src="../images/a11y-node-ARIA.png" alt="Image showing how a div with role=checkbox might simultaneously be drawn as a custom checkbox, while mapping onto a native chcekbox in the accessibility API.">
    </article>

    <article>
      <h3>
        Other platform accessibility APIs
      </h3>
      <p>
        Typically, most platforms allow developers to use a
        <strong>programmatic API</strong>
        to create virtual accessibility trees to implement accessibility for custom views.
      </p>
      <p>
        The web platform requires
        <em>everything</em> to do with accessibility to be specified declaratively, even as many parts of the web platform are
        moving to more dynamic, imperative style APIs.
      </p>
    </article>

    <article>
      <h1>
        Motivating Use Cases
      </h1>
    </article>


    <article>
      <h3>
        Use Case 1: Setting non-reflected (“default”) accessibility properties for <a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components">Web Components</a>
      </h3>
      <p>
        Today, a library author creating a custom element is forced to "sprout" ARIA attributes to express semantics which are implicit
        for native elements.
      </p>

      <pre>
    <!-- Page author uses the custom element as they would a native element -->
    <custom-slider min="0" max="5" value="3"></custom-slider>

    <!-- Custom element is forced to "sprout" extra attributes
         to express semantics -->
    <custom-slider min="0" max="5" value="3" role="slider"
                   tabindex="0"
                   aria-valuemin="0" aria-valuemax="5"
                   aria-valuenow="3" aria-valuetext="3"></custom-slider>
  </pre>
      <p>
        Unlike a native element, a custom element's semantics may be completely overwritten or removed by the author using the custom
        element, and it is impossible to distinguish between the semantics provided by the custom element and semantics set
        by the page author.
      </p>
      <p>
        This capability
        <em>need not</em>, but
        <em>may</em> be limited to Web Components.
      </p>
    </article>

    <article>
      <h3>
        Use Case 2: Setting relationships without IDREFs
      </h3>
      <p>
        To express an accessible relationship between two elements in the DOM, ARIA attributes let one element refer to the ID of
        another element.
      </p>
      <p>
        <code>aria-labelledby</code> indicates one element labels another:</p>
      <pre>
    <div id="firstname">First name:</div>
    <input aria-labelledby="firstname">
  </pre>
      <p>
        <p>
          <code>aria-activedescendant</code> indicates a descendant that's focused in a composite control like a list box.</p>
        <pre>
    <div role="listbox" aria-activedescendant="item1" tabindex="0">
      <div role="option" id="item1">Item 1</div>
      <div role="option" id="item2">Item 2</div>
      <div role="option" id="item3">Item 3</div>
    </div>
  </pre>
    </article>

    <article>
      <h3>
        Use Case 2: Setting relationships without IDREFs (2)
      </h3>
      <p>
        It can be
        <em>cumbersome</em> and
        <em>error-prone</em> to maintain unique IDs on all elements that have a relationship with some other element.
      </p>
      <p>
        But if you're using Shadow DOM, it's
        <em>impossible</em> to establish a relationship between two elements that aren't part of the same
        <em>tree scope</em>.
      </p>
      <p>
        For example, it's
        <em>impossible</em> to make an accessible combobox using
        <code>aria-activedescendant</code> if the text field is in a different tree scope from the listbox options:
      </p>
      <pre>
      <custom-combobox>
          #shadow-root (open)
          |  <!-- this doesn't work! -->
          |  <input aria-activedescendant="opt1"></input>
          |  <slot></slot>
          <custom-optionlist>
            <x-option id="opt1">Option 1</x-option>
            <x-option id="opt2">Option 2</x-option>
            <x-option id='opt3'>Option 3</x-option>
         </custom-optionlist>
        </custom-combobox>
  </pre>
    </article>

    <article>
      <h3>
        Use Case 3: Capturing input events from AT
      </h3>
      <p>
        ARIA only helps the web author communicate in one direction: from the web app to the assistive technology.
      </p>
      <p>
        However, AT can also command and control applications, for example issuing commands to activate, focus, or scroll. While
        native HTML elements already support these commands, web authors can't override this behavior for custom elements.
      </p>
      <p>
        Examples: AT wants to
        <b>select</b> an item in a list box,
        <b>increment</b> a slider, or
        <b>dismiss</b> a dialog.
      </p>
      <p>
        Another example: AT user performs a gesture to explicitly
        <b>scroll to the left</b> on a map widget.
      </p>
    </article>

    <article>
      <h3>
        Use Case 4: Making custom-drawn UI accessible.
      </h3>
      <p>
        To make a canvas-based UI (for example, a UI drawn using WebGL) accessible requires hacks like non-rendered fallback DOM
        content doing double duty as the accessible version of the painted UI.
      </p>
      <p>
        More generally, any UI which has a visual semantic structure (such as an image with complex content) which doesn't match
        up to its DOM structure is difficult to make accessible.
      </p>
    </article>

    <article>
      <h3>
        Use Case 5: Introspection
      </h3>
      <p>
        There's no way to programmatically detect if an accessibility feature is supported by the browser or if you're using it correctly.
      </p>
      <pre>
    <!-- illegal role - how do you know? -->
    <div role="chef"></div>
  </pre>

      <pre>
    <!-- attribute not allowed on that role - how do you know? -->
    <div role="listbox" aria-setsize="25"></div>
  </pre>
    </article>

    <article>
      <h1>
        Accessibility Object Model
      </h1>
    </article>

    <article>
      <h3>
        AOM: Brief History
      </h3>
      <p>
        Microsoft and Mozilla independently proposed a JavaScript accessibility API. Apple and Google had similar ideas.
      </p>
      <p>
        AOM is a collaboration between Apple, Google, and Mozilla, part of the Web Incubator Community Group. It takes the best ideas
        from previous proposals and organizes them into four
        <em>phases</em>, each with smaller scope, each solving an increasing number of real-world use-cases.
      </p>
      <p>
        Both Safari and Google Chrome have implemented some of the spec experimentally.
      </p>
    </article>

    <article>
      <h3>
        Warning!
      </h3>
      <p>
        All syntax shown on these slides is subject to change.
      </p>
      <p>
        We are committed to
        <em>solving these problems</em>, but the solution may not be exactly what you see here.
      </p>
    </article>

    <article>
      <h3>
        Phases 1-3 overview
      </h3>
      <p>
        Phases 1 through three, taken together, should bring the capabilities of accessibility on the web closer to what is possible
        on native platorms.
      </p>
      <ul>
        <li>Programmatic API
          <li>Virtual tree for custom UI
            <li>Listen to events from assistive technology
      </ul>
    </article>

    <article>
      <h3>
        Phase 1: Accessible Properties
      </h3>
      <p>
        Basically, reflect ARIA into JavaScript.
      </p>
      <p>
        Every ARIA attribute, such as
        <code>role</code> and
        <code>aria-checked</code> here:
      </p>

      <pre>
    <div role="checkbox" aria-checked="true">Receive promotional offers</div>
  </pre>

      <p>
        ...can be set directly from JavaScript:
      </p>

      <pre>
    el.role = "checkbox";
    el.ariaChecked = true;
  </pre>

    </article>
    <article>
      <h3>Phase 1: Accessible Properties (2)</h3>
      <p>
          These properties will also be available on the <a href="https://html.spec.whatwg.org/multipage/custom-elements.html#the-elementinternals-interface"><code>ElementInternals</code> interface</a>.
          This will allow Custom Elements to express their intrinsic semantics:
      </p>
      <pre>
          class CustomCheckbox extends HTMLElement {
            constructor() {
              super();
              this._internals = this.attachInternals();
              this._internals.role = "checkbox";  // Set the default semantics.
            }
          
            // When the custom "checked" attribute changes, keep the accessible checked state in sync.
            attributeChangedCallback(name, oldValue, newValue) {
              switch(name) {
              case "checked":
                this._internals.ariaChecked = (newValue !== null);
              }
            }
          }
          // Authors may use ARIA as usual: <custom-checkbox role="radio"></custom-checkbox>
          customElements.define("custom-checkbox", CustomCheckbox);
      </pre>

    </article>

    <article>
      <h3>
        Phase 1: Accessible Properties (3)
      </h3>
      <p>
        Instead of using an IDREF:
      </p>
      <pre>
    el.setAttribute('aria-activedescendant', 'child' + childIndex);
  </pre>
      <p>
        ...just reference another element directly:
      </p>
      <pre>
    el.ariaActiveDescendant = child;
  </pre>

    </article>

    <article>
      <h3>
        Phase 2: Background on events from AT
      </h3>
      <p>
        Problem: if you create an accessible element with
        <code>role=slider</code>, then interact with that element via a screen reader on a mobile device, you will hear instructions to increment
        or decrement the slider.
      </p>
      <p>
        However, there is no way for a web developer to implement the slider such that following those instructions will have the
        desired effect.
      </p>
    </article>

    <article>
      <h3>
        Phase 2: Background on events from AT
      </h3>
      <p>
        As a reminder, AT users are often not generating keyboard, mouse, and touch events.
      </p>
      <p>
        They issue high-level commands (click, focus, select, dismiss, etc.) directly to their AT, and the AT tells the app to take
        that action via accessibility APIs.
      </p>
    </article>

    <article>
      <h3>
        Phase 2: Accessible Actions / Input Events
      </h3>
      <p>
        To preserve the <a href="https://github.com/WICG/aom/issues/81">privacy</a> of assistive technology users,
        events from assistive technology will cause a synthesised DOM event to be triggered where appropriate:
      </p>
      <pre>
    slider.addEventListener("keydown", function(event) {
      // Keydown event may be generated via keyboard or via assistive technology gesture
      console.log("Got keydown event");
      switch (event.code) {
        case "ArrowUp":
          customSlider.value += 1;
          return;
        case "ArrowDown":
          customSlider.value -= 1;
          return;
      }
    });
    </article>

    <article>
      <h3>
        Phase 2: Accessible Actions / Input Events
      </h3>
      <p>
        New input event types capturing common patterns will also be added.
        Where supported, these new event types will be triggered any user interactions which
        triggers the same events as those which would be fired as a fallback.
      </p>
      <p>
        For example, if an "increment" gesture from AT triggers a
        <code>keydown</code>/<code>keyup</code> sequence for the right arrow key,
        then if a user presses the right arrow key while focused on an applicable element,
        an <code>increment</code> event will also be fired.
      </p>
      <pre>
    // "increment" or "decrement" may be triggered by an AT gesture, by a keyboard event or 
    // by a specific interaction such as clicking a button

    slider.addEventListener("increment", function(event) {
      customSlider.value += 1;
    });

    slider.addEventListener("decrement", function(event) {
      customSlider.value -= 1;
    });
</pre>
    </article>

    <!--
    <article>
      <h3>Phase 2 demo: Canvas slider</h3>
      <style>
        canvas {
          margin-left: auto;
          margin-right: auto;
          display: block;
          width: 400px;
          height: 120px;
        }
      </style>
      <p>
        Using VoiceOver, try
        <i>Interacting with</i> this slider and using VoiceOver commands to increment and decrement it.
      </p>
      <canvas id="slider" width="400" height="120" value="5" min="1" max="10">
      </canvas>
      <script>
        let canvas = document.getElementById('slider');
        window.updateSlider = function (delta) {
          let min = parseInt(canvas.getAttribute('min'), 10);
          let max = parseInt(canvas.getAttribute('max'), 10);
          let value = parseInt(canvas.getAttribute('value'), 10);
          value += delta;
          if (value > max)
            value = max;
          if (value < min)
            value = min;
          canvas.accessibleNode.valueText = "" + value;
          canvas.accessibleNode.valueNow = value;
          canvas.setAttribute('value', value);
        }
        canvas.addEventListener('keydown', function (event) {
          if (event.key == 'ArrowLeft')
            updateSlider(-1);
          if (event.key == 'ArrowRight')
            updateSlider(+1);
        });

        window.setInterval(function () {
          let canvas = document.getElementById('slider');
          let context = canvas.getContext('2d');
          let margin = 30;
          let left = margin;
          let top = margin;
          let width = canvas.offsetWidth - 2 * margin;
          let height = canvas.offsetHeight - 2 * margin;
          let right = left + width;
          let bottom = top + height;
          let cy = top + height / 2;
          let r = 10;
          let rr = r / 2;
          context.globalAlpha = 1.0;
          context.fillStyle = '#ffffff';
          context.fillRect(0, 0, canvas.offsetWidth, canvas.offsetHeight);
          context.lineWidth = 1;
          context.globalAlpha = 1.0;

          context.beginPath();
          context.moveTo(left - r, cy);
          context.bezierCurveTo(left - r, cy - rr, left - rr, cy - r, left, cy - r);
          context.lineTo(right, cy - r);
          context.bezierCurveTo(right + rr, cy - r, right + r, cy - rr, right + r, cy);
          context.bezierCurveTo(right + r, cy + rr, right + rr, cy + r, right, cy + r);
          context.lineTo(left, cy + r);
          context.bezierCurveTo(left - rr, cy + r, left - r, cy + rr, left - r, cy);
          context.shadowColor = '#555577';
          context.shadowBlur = 10;
          context.fillStyle = '#aaaaee';
          context.fill();

          let min = parseInt(canvas.getAttribute('min'), 10);
          let max = parseInt(canvas.getAttribute('max'), 10);
          let value = parseInt(canvas.getAttribute('value'), 10);
          let steps = max - min;
          for (let i = min; i <= max; i++) {
            let x = left + (i - min) * (width / steps);
            context.beginPath();
            context.ellipse(x, cy, 3, 3, 0, 0, 2 * Math.PI, true);
            context.shadowColor = '#ffffff';
            context.shadowBlur = 2;
            context.fillStyle = '#ffffff';
            context.fill();
            context.font = "14px sans-serif";
            let text = "" + i;
            let metrics = context.measureText(text);
            context.fillStyle = '#000000';
            context.fillText(text, x - metrics.width / 2, cy + 3 * r);
          }
          let x = left + (value - min) * (width / steps);
          let cy1 = cy - 10;
          let cy2 = cy + 30;
          context.beginPath();
          context.moveTo(x - r, cy1);
          context.bezierCurveTo(x - r, cy1 - rr, x - rr, cy1 - r, x, cy1 - r);
          context.bezierCurveTo(x + rr, cy1 - r, x + r, cy1 - rr, x + r, cy1);
          context.lineTo(x + r, cy2);
          context.bezierCurveTo(x + r, cy2 + rr, x + rr, cy2 + r, x, cy2 + r);
          context.bezierCurveTo(x - rr, cy2 + r, x - r, cy2 + rr, x - r, cy2);
          context.lineTo(x - r, cy1);
          context.lineWidth = 3;
          context.strokeStyle = '#ff6666';
          context.shadowColor = '#ff6666';
          context.shadowBlur = 2;
          context.globalAlpha = 0.5;
          context.stroke();
        }, 50);

        $("slider").tabIndex = 0;
        $("slider").accessibleNode.role = "slider";
        $("slider").accessibleNode.valueMin = 1;
        $("slider").accessibleNode.valueMax = 10;
        $("slider").accessibleNode.valueNow = 5;
        $("slider").accessibleNode.addEventListener(
          "accessibleincrement", function () {
            updateSlider(1);
          });
        $("slider").accessibleNode.addEventListener(
          "accessibledecrement", function () {
            updateSlider(-1);
          });

      </script>
      <p>
        Code skeleton:
      </p>
      <pre>
  slider.tabIndex = 0;
  slider.role = "slider";
  slider.ariaValueMin = 1;
  slider.ariaValueMax = 10;
  slider.ariaValueNow = 5;
  slider.addEventListener(
      "accessibleincrement", function() {
    updateSlider(1);
  });
  slider.addEventListener(
      "accessibledecrement", function() {
    updateSlider(-1);
  });
</pre>
    </article>
  -->
    <article>
      <h3>
        Phase 3: Virtual Nodes
      </h3>
      <p>
        You can
        <b>construct</b> a "virtual" AccessibleNode that isn't associated with any DOM element.
      </p>
      <pre>
    let virtualNode = new AccessibleNode();
    virtualNode.role = "button";
    virtualNode.label = "Play Game";
  </pre>
      <p>
        Insert an AccessibleNode as the child of a DOM element or another AccessibleNode:
      </p>
      <pre>
    // This replaces all DOM children of document.body for the purposes
    // of accessibility tree computation.
    document.body.attachAccessibleRoot();
    document.body.accessibleRoot.appendChild(virtualNode);
  </pre>
    </article>

    <article>
      <h3>
        Restrictions on virtual nodes
      </h3>
      <p>
        You can't use these methods to rearrange the accessibility tree corresponding to the DOM.
      </p>
      <p>
        You can only build a "virtual" accessibility tree that "hangs off" of one DOM node.
      </p>
    </article>

    <article>
      <h3>
        Bounding boxes
      </h3>
      <p>
        To set the bounding box of a virtual node, provide its left, top, width, and height in pixels, relative to its
        <code>offsetParent</code>, which can be any ancestor AccessibleNode.
      </p>
      <pre>
    virtualNode.offsetLeft = 30;
    virtualNode.offsetTop = 20;
    virtualNode.offsetWidth = 400;
    virtualNode.offsetHeight = 300;
    virtualNode.offsetParent = document.body.accessibleRoot.firstChild;
  </pre>
      <p>
        Relative coordinates are important for things like a canvas-based UI, because the canvas element might have CSS transforms
        applied to it.
      </p>
    </article>

    <article>
      <h3>
        Virtual AccessibleNode demo
      </h3>
      <div style="width: 200px; margin: 200px auto;">
        <a href="tictactoe.html" target="_blank">Tic-tac-toe</a>
      </div>
    </article>

    <!--
<article>
  <h3>
    Focus management
  </h3>
  <p>
    To make a node focusable, the <code>focusable</code> attribute can
    be set. This is similar to setting tabIndex=-1 on a DOM element.
  </p>
  <pre>
    virtualNode.focusable = true;
  </pre>
  <p>
    To focus an accessible node, call its <code>focus()</code> method.
  </p>
  <pre>
    virtualNode.focus();
  </pre>
  <p>
    When a virtual accessible node is focused, input focus in the DOM
    is unchanged. The focused accessible node is reported to assistive
    technology and other accessibility API clients, but no DOM events
    are fired and document.activeElement is unchanged.
  </p>
</article>

<article>
  <h3>
    Selection management
  </h3>
  <p>
    To select text in an accessible node, call its <code>select()</code> method.
  </p>
  <pre>
    virtualNode.select(5, 10);
  </pre>
  <p>
    When a virtual accessible node has a selection set in it,
    selection in the DOMis unchanged. The accessible selection is
    reported to assistive technology and other accessibility API
    clients, but no DOM events are fired and window.selection is
    unchanged.
  </p>
</article>
-->

    <article>
      <h3>
        Phase 4: Introspection
      </h3>
      <p>
        Every AccessibleNode has a corresponding
        <code>ComputedAccessibleNode</code>.
      </p>
      <p>
        Accessing it is asynchronous (via a Promise), and it gives you full access to that node, and the accessibility tree.
      </p>
      <pre>
    let computed = await getComputedAccessibleNode(myListItem);
    computed.role;             // "listitem"
    computed.name;             // "Picard"
    computed.parent.role;      // "list"
    computed.children.length;  // 0
  </pre>
    </article>

    <article>
      <h3>
        Phase 4: Feature detection
      </h3>
      <p>
        The
        <code>ComputedAccessibleNode</code> enables you to determine if a role, or any other accessibility feature, is supported.
      </p>
      <pre>
    let button = document.createElement("button");
    button.role = "hammer";
    let computed = await getComputedAccessibleNode(button);
    computed.role;             // "button" (not "hammer")
  </pre>
    </article>

    <article>
      <h3>
        Phase 4: Walking the tree
      </h3>
      <p>
        Walk the accessibility tree for testing or debugging.
      </p>
      <pre>
    function printAccessibilityTree(computed, indent) {
      print(spaces(indent) + 'Role: ' + computed.role + ', Name: ' + computed.name);
      for (let i = 0; i < computed.children.length; i++) {
        printAccessibilityTree(computed.children[i], indent + 1);
      }
    }

    let computed = await getComputedAccessibleNode(document.body);
    printAccessibilityTree(computed, 0);
  </pre>
    </article>

    <article>
      <h3>
        Phase 4: Writing in-browser AT
      </h3>
      <p>
        Write a screen reader or other AT
      </p>
      <div style="width: 400px; margin: 200px auto;">
        <a href="screen_reader.html" target="_blank">AOM Screen Reader</a>
      </div>
    </article>

    <article>
      <h1>
        For more information:
        <br>
        <a href="http://github.com/WICG/aom">github.com/WICG/aom</a>
      </h1>

      <h3 style="margin-top: 100px">
        Questions?
      </h3>
      <p>
    </article>
  </section>

</body>

</html>
